package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.RedisData;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClient;
import io.netty.util.internal.StringUtil;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    public CacheClient cacheClient;


    /*
     * 实现redis缓存
     * */
    @Override
    @Transactional
    public Result queryById(Long id) {
        Shop shop ;
        //缓存雪崩
//        shop = cacheClient.<Shop, Long>queryWithCacheAvalanche(CACHE_SHOP_KEY, id, Shop.class, this::getById, 60L, TimeUnit.MINUTES);

        //缓存击穿-存null
//        shop = cacheClient.<Shop, Long>queryWithRassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, 60L, TimeUnit.MINUTES);

        //缓存穿透-互斥锁
//        shop=cacheClient.<Shop, Long>queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, 60L, TimeUnit.MINUTES);

        //缓存穿透-逻辑删除
        shop=cacheClient.<Shop, Long>queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, idd->this.getById(idd), 60L, TimeUnit.MINUTES);

        return Result.ok(JSONUtil.toJsonStr(shop));
    }

    /*
     * 实现redis缓存解决缓存穿透
     * */
    @Override
    @Transactional
    public Result cachWalkthrough(Long id) {

        //1、判断id是否为空，空则说明有问题
        if (ObjectUtils.isEmpty(id)) {
            return Result.fail("服务器异常");
        }

        //2、从redis查询数据
        String key = CACHE_SHOP_KEY + id;
        String shopStr = stringRedisTemplate.opsForValue().get(key);

        //3、判断如果当前redis有缓存
        if (StrUtil.isNotBlank(shopStr)) {
            //这里要将json串转回bean
            Shop shop = JSONUtil.toBean(shopStr, Shop.class);
            return Result.ok(shop);
        }

        //新增判断，查到了，但是是""
        if (shopStr != null) {
            return Result.fail("无此数据");
        }

        //4、没有缓存，去mysql中查询
        Shop shop = this.getById(id);

        //5、查出来是空,这个时候也要放进redis
        if (BeanUtil.isEmpty(shop)) {
            //存个空值进去
            stringRedisTemplate.opsForValue().set(key, "", 1, TimeUnit.HOURS);
            return Result.fail("无此数据");
        }

        //6、弄成json串
        String jsonStr = JSONUtil.toJsonStr(shop);

        //7、存进redis，设置一小时过期
        stringRedisTemplate.opsForValue().set(key, jsonStr, 1, TimeUnit.HOURS);

        return Result.ok(jsonStr);
    }

    /*
     * 实现redis缓存解决缓存雪崩
     * */
    @Override
    @Transactional
    public Result cacheAvalanche(Long id) {

        //1、判断id是否为空，空则说明有问题
        if (ObjectUtils.isEmpty(id)) {
            return Result.fail("服务器异常");
        }

        //2、从redis查询数据
        String key = CACHE_SHOP_KEY + id;
        String shopStr = stringRedisTemplate.opsForValue().get(key);

        //3、判断如果当前redis有缓存
        if (StrUtil.isNotBlank(shopStr)) {
            //这里要将json串转回bean
            Shop shop = JSONUtil.toBean(shopStr, Shop.class);
            return Result.ok(shop);
        }

        //新增判断，查到了，但是是""
        if (shopStr != null) {
            return Result.fail("无此数据");
        }

        //4、没有缓存，去mysql中查询
        Shop shop = this.getById(id);

        //5、查出来是空,这个时候也要放进redis
        if (BeanUtil.isEmpty(shop)) {
            //存个空值进去
            stringRedisTemplate.opsForValue().set(key, "", 60 + RandomUtil.randomInt(-10, 10), TimeUnit.MINUTES);
            return Result.fail("无此数据");
        }

        //6、弄成json串
        String jsonStr = JSONUtil.toJsonStr(shop);

        //7、存进redis，设置一小时过期
        stringRedisTemplate.opsForValue().set(key, jsonStr, 60 + RandomUtil.randomInt(-10, 10), TimeUnit.MINUTES);

        return Result.ok(jsonStr);
    }

    /*
     * 实现redis缓存解决缓存击穿---互斥锁
     * */
    @Override
    @Transactional
    public Result queryWithMutex(Long id) {
        //1、判断id是否为空，空则说明有问题
        if (ObjectUtils.isEmpty(id)) {
            return Result.fail("服务器异常");
        }

        //2、从redis查询数据
        String key = CACHE_SHOP_KEY + id;
        String shopStr = stringRedisTemplate.opsForValue().get(key);

        //3、判断如果当前redis有缓存
        if (StrUtil.isNotBlank(shopStr)) {
            //这里要将json串转回bean
            Shop shop = JSONUtil.toBean(shopStr, Shop.class);
            return Result.ok(shop);
        }
        String jsonStr;
        try {
            //7、先获得锁,拿不到的时候
            while (!tryLock(LOCK_SHOP_KEY + id)) {
                //8、休眠一下子
                Thread.sleep(50);

                //9、休眠完查一下现在有没有，这个重复代码，但是我不想用递归，后期封装起来就行了
                shopStr = stringRedisTemplate.opsForValue().get(key);
                System.out.println(Thread.currentThread().getId() + "等待中~~~~");


                //10、判断如果当前redis有缓存
                if (StrUtil.isNotBlank(shopStr)) {
                    //这里要将json串转回bean
                    Shop shop = JSONUtil.toBean(shopStr, Shop.class);
                    System.out.println(Thread.currentThread().getId() + "等待的时候别人缓存了，我不等了~~~~");
                    return Result.ok(shop);
                }

            }

            System.out.println(Thread.currentThread().getId() + "已获得锁~~~~");

            //11、拿到锁了，不过还是要再判断一次！！防止死锁
            shopStr = stringRedisTemplate.opsForValue().get(key);

            //12、判断如果当前redis有缓存，则还是不需要去数据库查
            if (StrUtil.isNotBlank(shopStr)) {
                System.out.println(Thread.currentThread().getId() + "已获得锁，但也有数据~~~~");
                //这里要将json串转回bean
                Shop shop = JSONUtil.toBean(shopStr, Shop.class);
                //直接释放锁
                unlock(LOCK_SHOP_KEY + id);
                return Result.ok(shop);
            }

            System.out.println(Thread.currentThread().getId() + "无缓存~~~~");
            //13、拿到锁且没有缓存，去mysql中查询
            Shop shop = this.getById(id);

            //模拟重建时的延迟
            Thread.sleep(200);

            //14、查出来是空
            if (BeanUtil.isEmpty(shop)) {
                return Result.fail("无此数据");
            }

            //15、弄成json串
            jsonStr = JSONUtil.toJsonStr(shop);

            //16、存进redis，设置一小时过期
            stringRedisTemplate.opsForValue().set(key, jsonStr, 1, TimeUnit.HOURS);
            System.out.println(Thread.currentThread().getId() + "已更新完毕");

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //17、释放锁
            unlock(LOCK_SHOP_KEY + id);
        }
        return Result.ok(jsonStr);
    }

    //这里需要声明一个线程池，因为下面我们需要新建一个现成来完成重构缓存
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /*
     * 实现redis缓存解决缓存击穿---逻辑过期
     * */
    @Override
    @Transactional
    public Result queryWithLogicalExpire(Long id) {
        //1、判断id是否为空，空则说明有问题
        if (ObjectUtils.isEmpty(id)) {
            return Result.fail("服务器异常");
        }

        //2、从redis查询数据
        String key = CACHE_SHOP_KEY + id;
        String shopStr = stringRedisTemplate.opsForValue().get(key);

        //3、判断如果当前redis无缓存，这样直接返回是因为，热点key数据都是提前放入数据库，不可能没有
        if (StrUtil.isBlank(shopStr)) {
            return Result.fail("无此数据");
        }

        //4、命中缓存，取出
        RedisData<Shop> redisData = JSONUtil.toBean(shopStr, RedisData.class);
        Shop shop = redisData.getData();

        //5、获得过期时间
        LocalDateTime expireTime = redisData.getExpireTime();

        //6、对比一下是否过期
        if (LocalDateTime.now().isBefore(expireTime)) {
            //未过期，直接返回
            return Result.ok(shop);
        }


        //过期了，来拿锁更新
        //7、先获得锁,拿不到的时候
        if (tryLock(LOCK_SHOP_KEY + id)) {
            //8、线程池开启多线程
            CACHE_REBUILD_EXECUTOR.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        //9、调用方法查询数据库并保存进redis中
                        saveShop2Redis(id, LOCK_SHOP_TTL);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    } finally {
                        //10、关锁
                        unlock(LOCK_SHOP_KEY + id);
                    }
                }
            });
            //11、返回旧数据
            return Result.ok(shop);
        }

        //12、拿不到锁，直接返回旧数据
        return Result.ok(shop);
    }

    /*
     * 用于设置逻辑过期查询并存储的
     * */
    public void saveShop2Redis(Long id, Long expirSeconds) {
        Shop shop = getById(id);
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expirSeconds));
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
    }

    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        //避免返回值为null，我们这里使用了BooleanUtil工具类
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }


    @Override
    public Result update(Shop shop) {
        //1、判断是否为空
        if (ObjectUtils.isEmpty(shop)) {
            return Result.fail("更新失败");
        }

        //2、更新数据（操作数据库）
        boolean flag = updateById(shop);

        //3、如果更新失败，
        if (!flag) {
            return Result.fail("更新失败");
        }

        //4、删除缓存数据（操作缓存）
        stringRedisTemplate.delete(CACHE_SHOP_KEY + shop.getId());

        return Result.ok();
    }
}
